package com.zjw.socket.demo.starter.core.ioc;


import com.zjw.socket.demo.starter.annotation.NonNull;
import com.zjw.socket.demo.starter.annotation.Nullable;
import com.zjw.socket.demo.starter.annotation.core.Bean;
import com.zjw.socket.demo.starter.annotation.core.Inject;
import com.zjw.socket.demo.starter.core.Reflector;

import java.lang.reflect.*;
import java.util.*;

class IOCScopeImpl implements IOCScope {

  private final Map<String, IOCBeanInfo> iocCollection = new HashMap<>();

  private final Set<Class<?>> refreshRecord = new HashSet<>();

  private final List<IOCObserver> refreshObserverList = new ArrayList<>();

  @Override
  public void registerRefreshObserver(IOCObserver iocObserver) {
    refreshObserverList.add(iocObserver);
  }

  @Override
  public void refresh(String basePackage) {

    iocCollection.clear();
    //return all class for this basePackage
    List<Class<?>> classes = Reflector.reflectPackageClass(basePackage, null);

    for (Class<?> aClass : classes) {
      doRefresh(aClass, false);
      notifyOnRefresh(aClass);
    }

    for (IOCBeanInfo value : iocCollection.values()) {
      doInject(value);
      notifyOnInject(value);
    }

    notifyOnComplete();
  }

  private void notifyOnComplete() {
    for (IOCObserver iocObserver : refreshObserverList) {
      iocObserver.onComplete();
    }
  }

  /**
   *
   */
  private void doRefresh(@NonNull Class<?> clazz, boolean isInject) {

    //单个类只刷一次
    if (refreshRecord.contains(clazz)) {
      if (isInject) {
        throw new IllegalArgumentException("each inject...");
      }
      return;
    }
    refreshRecord.add(clazz);

    String className = clazz.getName();
    String objectKey = className;

    //Class
    Bean bean = clazz.getAnnotation(Bean.class);
    if (bean != null) {
      String beanName = bean.name();
      objectKey = "".equals(beanName) ? objectKey : beanName;
      Object o = Reflector.instanceEmptyConstructor(clazz);
      if (o == null) {
        throw new IllegalArgumentException("can't invoke class with out empty constructor " + clazz);
      }
      finishRefresh(objectKey, bean, o);
    }

    //Constructor
    Constructor<?>[] constructors = clazz.getConstructors();
    for (Constructor<?> constructor : constructors) {
      bean = constructor.getAnnotation(Bean.class);
      if (bean == null) {
        continue;
      }
      String beanName = bean.name();
      objectKey = "".equals(beanName) ? className : beanName;
      if (iocCollection.containsKey(objectKey)) {
        throw new IllegalArgumentException("repeat bean name " + objectKey);
      }
      Object[] instanceParameter = resolveInstanceParams(constructor.getParameters(), objectKey);
      Object o = Reflector.instanceConstructor(constructor, instanceParameter);
      finishRefresh(objectKey, bean, o);
    }

    //Method
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
      bean = declaredMethod.getAnnotation(Bean.class);
      if (bean == null) {
        continue;
      }
      Class<?> returnType = declaredMethod.getReturnType();
      if (!Modifier.isStatic(declaredMethod.getModifiers())) {
        throw new IllegalArgumentException("@Bean Method must be static");
      }
      objectKey = "".equals(bean.name()) ? returnType.getName() : bean.name();
      if (iocCollection.containsKey(objectKey)) {
        throw new IllegalArgumentException("repeat bean name " + objectKey);
      }

      Object[] instanceParameter = resolveInstanceParams(declaredMethod.getParameters(), objectKey);
      Object o = Reflector.invokeStaticMethod(declaredMethod, instanceParameter);
      if (o == null) {
        throw new IllegalArgumentException("can't invoke class " + returnType);
      }
      finishRefresh(objectKey, bean, o);
    }
  }

  private void finishRefresh(String objectKey, Bean bean, Object o) {
    Class<?> type = bean.type().equals(Object.class) ? o.getClass() : bean.type();
    if (!type.isInstance(o)) {
      throw new IllegalArgumentException("@Bean error:" + o + " is not " + type);
    }

    IOCBeanInfo info = new IOCBeanInfo(objectKey, o, type);
    info.state = IOCBeanInfo.STATE_REFRESHING;
    iocCollection.put(objectKey, info);
  }

  private Object[] resolveInstanceParams(@NonNull Parameter[] parameters, @Nullable String holder) {
    Object[] objects = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
      Parameter parameter = parameters[i];
      Inject inject = parameter.getAnnotation(Inject.class);
      if (inject != null) {
        String query = inject.query();
        if ("".equals(query)) {
          query = parameter.getType().getName();
        }
        IOCBeanInfo o = iocCollection.get(query);
        if (o != null) {
          objects[i] = o.object;
          continue;
        }
        doRefresh(parameter.getType(), true);
        IOCBeanInfo iocBeanInfo = iocCollection.get(query);
        if (iocBeanInfo == null) {
          throw new IllegalArgumentException("not find bean name" + query);
        }
        objects[i] = iocBeanInfo.object;
      } else {
        throw new IllegalArgumentException("bean:" + holder + " not find @Inject ");
      }
    }
    return objects;
  }


  private void doInject(IOCBeanInfo info) {
    Field[] declaredFields = info.object.getClass().getDeclaredFields();
    info.state = IOCBeanInfo.STATE_INJECTING;
    for (Field declaredField : declaredFields) {
      Inject inject = declaredField.getAnnotation(Inject.class);
      if (inject == null) {
        continue;
      }
      Class<?> type = declaredField.getType();
      String query = "".equals(inject.query()) ? type.getName() : inject.query();

      IOCBeanInfo o = iocCollection.get(query);
      if (o == null) {
        throw new IllegalArgumentException("bad inject source for " + info.object.getClass() + " field " + declaredField.getName() + " query " + query);
      }
      Reflector.safeSetFiled(info.object, declaredField, o.object);
      o.incomeRefer.add(new InjectRecord(info, declaredField));
    }
    info.state = IOCBeanInfo.STATE_COMPLETE;
  }


  private void notifyOnRefresh(@NonNull Class<?> clazz) {
    for (IOCObserver observer : refreshObserverList) {
      observer.onRefresh(clazz);
    }
  }

  private void notifyOnInject(@NonNull IOCBeanInfo value) {
    for (IOCObserver observer : refreshObserverList) {
      observer.onInject(value);
    }
  }

  @Override
  @Nullable
  public IOCBeanInfo getBeanInfo(String objectKey) {
    return iocCollection.get(objectKey);
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T> T getBean(Class<T> clazz) {
    Object o = iocCollection.get(clazz.getName()).object;
    return (T) o;
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T> T getBean(String name) {
    Object o = iocCollection.get(name).object;
    return (T) o;
  }

  @Override
  public void putBean(Class<?> clazz, Object object) {
    if (iocCollection.containsKey(clazz.getName())) {
      throw new IllegalArgumentException("Bean has contain " + clazz);
    }
    iocCollection.put(clazz.getName(), new IOCBeanInfo(clazz.getName(), object, clazz));
  }

  @Override
  public void putBean(String name, Object object) {
    if (iocCollection.containsKey(name)) {
      throw new IllegalArgumentException("Bean has contain " + name);
    }
    iocCollection.put(name, new IOCBeanInfo(name, object));
  }
}
